package jmine.tec.utils.io.http.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Map.Entry;

import jmine.tec.utils.Tuple;
import jmine.tec.utils.collection.MultiMap;
import jmine.tec.utils.collection.impl.EmptyMultiMap;
import jmine.tec.utils.collection.impl.ListMultiMap;
import jmine.tec.utils.io.EncodingUtils;
import jmine.tec.utils.io.IOUtils;
import jmine.tec.utils.io.ReversibleOutputStream;
import jmine.tec.utils.io.StringInputStream;
import jmine.tec.utils.io.http.Connector;
import jmine.tec.utils.io.http.HttpMethod;
import jmine.tec.utils.io.http.exception.HttpClientErrorException;
import jmine.tec.utils.io.http.exception.HttpException;
import jmine.tec.utils.io.http.exception.HttpServerErrorException;
import jmine.tec.utils.io.http.exception.RetryRequestException;

/**
 * Implementacao abstrata de Connector. Subclasses precisam implementar apenas
 * {@link #doMakeRequestWithBody(String, HttpMethod, MultiMap, MultiMap, InputStream)}
 * 
 * @author takeshi
 */
public abstract class AbstractConnector implements Connector {

    private static final String CONTENT_TYPE = "Content-Type";

    public static final int BUFFER_LIMIT = 10 * 1024;

    public static final String HEADER_ENCODING = "UTF-8";

    private static final byte[] FILENAME = EncodingUtils.toASCII("; filename=");

    private static final byte[] CRLF = EncodingUtils.toASCII("\r\n");

    private static final byte[] SEPARATOR = EncodingUtils.toASCII("--");

    private static final byte[] CONTENT_DISPOSITION = EncodingUtils.toASCII("Content-Disposition: form-data; name=");

    private static final byte[] QUOTE = EncodingUtils.toASCII("\"");

    private static final int NUM_5 = 5;

    private static final int NUM_4 = 4;

    private static final int NUM_100 = 100;

    public static final int MAX_RETRIES_COUNT = 5;

    private static final MultiMap<String, String> EMPTY_MAP = EmptyMultiMap.getInstance();

    private static final byte[] MULTIPART_CHARS = EncodingUtils.toASCII("-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");

    /**
     * {@inheritDoc}
     */
    public InputStream makeDelete(String path, Map<String, String> params) throws IOException, HttpException {
        return makeRequestWithBody(path, HttpMethod.DELETE, EMPTY_MAP, toMultiMap(params), null).head();
    }

    /**
     * Converte um mapa de parametros em um {@link MultiMap} de parametros
     * 
     * @param params {@link Map}
     * @return {@link MultiMap}
     */
    private MultiMap<String, String> toMultiMap(Map<String, String> params) {
        ListMultiMap<String, String> queryParameters = new ListMultiMap<String, String>();
        if (params != null && !params.isEmpty()) {
            queryParameters.addAll(params);
        }
        return queryParameters;
    }

    /**
     * {@inheritDoc}
     */
    public Reader makeDeleteStringResponse(String path, Map<String, String> params) throws IOException, HttpException {
        return makeRequestStringResponse(path, HttpMethod.DELETE, EMPTY_MAP, toMultiMap(params), null);
    }

    /**
     * {@inheritDoc}
     */
    public InputStream makeGet(String path, Map<String, String> params) throws IOException, HttpException {
        return makeRequest(path, HttpMethod.GET, EMPTY_MAP, toMultiMap(params)).head();
    }

    /**
     * {@inheritDoc}
     */
    public Reader makeGetStringResponse(String path, Map<String, String> params) throws IOException, HttpException {
        return makeRequestStringResponse(path, HttpMethod.GET, EMPTY_MAP, toMultiMap(params));
    }

    /**
     * {@inheritDoc}
     */
    public InputStream makePost(String path, Map<String, String> params) throws IOException, HttpException {
        return makeRequestWithBody(path, HttpMethod.POST, getPostHeaderParameters(), EMPTY_MAP,
                new StringInputStream(encodeQueryParameters(toMultiMap(params)), HEADER_ENCODING)).head();
    }

    /**
     * {@inheritDoc}
     */
    public Reader makePostStringResponse(String path, Map<String, String> params) throws IOException, HttpException {
        return makeRequestStringResponse(path, HttpMethod.POST, getPostHeaderParameters(), EMPTY_MAP, new StringInputStream(
                encodeQueryParameters(toMultiMap(params)), HEADER_ENCODING));
    }

    /**
     * {@inheritDoc}
     */
    public InputStream makePut(String path, Map<String, String> params) throws IOException, HttpException {
        return makeRequestWithBody(path, HttpMethod.PUT, getPostHeaderParameters(), toMultiMap(params), null).head();
    }

    /**
     * {@inheritDoc}
     */
    public Reader makePutStringResponse(String path, Map<String, String> params) throws IOException, HttpException {
        return makeRequestStringResponse(path, HttpMethod.PUT, getPostHeaderParameters(), toMultiMap(params), null);
    }

    /**
     * Cria um mapa com headers para um post, setando apenas o Content-Type
     * 
     * @return {@link ListMultiMap}
     */
    private ListMultiMap<String, String> getPostHeaderParameters() {
        ListMultiMap<String, String> map = new ListMultiMap<String, String>();
        map.add(CONTENT_TYPE, "application/x-www-form-urlencoded");
        map.add(CONTENT_TYPE, "charset=UTF-8");
        return map;
    }

    /**
     * Faz o encode dos query parameters para URL
     * 
     * @param queryParams map
     * @return String
     * @throws UnsupportedEncodingException e
     */
    protected String encodeQueryParameters(MultiMap<String, String> queryParams) throws UnsupportedEncodingException {
        StringBuilder sb = new StringBuilder();
        Iterator<Entry<String, Collection<String>>> iterator2 = queryParams.entries().iterator();
        while (iterator2.hasNext()) {
            Entry<String, Collection<String>> entry = iterator2.next();
            String key = URLEncoder.encode(entry.getKey(), "UTF-8");
            String separator = "&";
            for (String value : entry.getValue()) {
                sb.append(key);
                sb.append("=");
                sb.append(URLEncoder.encode(value, "UTF-8"));
                sb.append(separator);
            }
        }
        return sb.toString();
    }

    /**
     * se o metodo devolveu algo diferente de 200, lanca uma excecao. Caso contrario nao faz nada
     * 
     * @param responseCode int
     * @param responseText String
     * @param path String
     * @param params params
     * @param errorBody the error body
     * @throws HttpException e
     */
    protected void handleResponse(int responseCode, String responseText, String path, MultiMap<String, String> params, String errorBody)
            throws HttpException {
        switch (responseCode / NUM_100) {
        case 2:
            // ok
            break;
        case NUM_4:
            this.handleClientError(responseCode, responseText, path, params, errorBody);
            break;
        case NUM_5:
            this.handleServerError(responseCode, responseText, path, params, errorBody);
            break;
        default:
            this.handleNonSpecifiedError(responseCode, responseText, path, params, errorBody);
            break;
        }
    }

    /**
     * Lanca uma excecao generica para um erro http nao especificado
     * 
     * @param statusCode o status code
     * @param statusText o text
     * @param path string
     * @param params params
     * @param errorBody String
     */
    protected void handleNonSpecifiedError(int statusCode, String statusText, String path, MultiMap<String, String> params, String errorBody) {
        throw new RuntimeException("unknown http error: " + statusCode + " with reason: " + statusCode + " on path: " + path
                + " with params: " + params + " and error body: " + errorBody);
    }

    /**
     * lida com erros http 5xx
     * 
     * @param statusCode o codigo
     * @param statusText o texto
     * @param path string
     * @param params params
     * @param errorBody String
     * @throws HttpException e
     */
    protected void handleServerError(int statusCode, String statusText, String path, MultiMap<String, String> params, String errorBody)
            throws HttpException {
        HttpServerErrorException httpServerErrorException = new HttpServerErrorException(statusCode, statusText);
        if (errorBody != null) {
            httpServerErrorException.setErrorBody(errorBody);
        }
        throw httpServerErrorException;
    }

    /**
     * lida com erros http 4xx
     * 
     * @param statusCode o codigo
     * @param statusText o texto
     * @param path string
     * @param params params
     * @param errorBody String
     * @throws HttpClientErrorException e
     */
    protected void handleClientError(int statusCode, String statusText, String path, MultiMap<String, String> params, String errorBody)
            throws HttpClientErrorException {
        final String suffix = " for path: " + path + " with params: " + params;
        if (statusText == null || "".equals(statusText)) {
            String cause = HttpClientErrorException.getMessage(statusCode);
            HttpClientErrorException httpClientErrorException = new HttpClientErrorException(statusCode, cause + suffix);
            if (errorBody != null) {
                httpClientErrorException.setErrorBody(errorBody);
            }
            throw httpClientErrorException;
        } else {
            HttpClientErrorException httpClientErrorException = new HttpClientErrorException(statusCode, statusText + suffix);
            if (errorBody != null) {
                httpClientErrorException.setErrorBody(errorBody);
            }
            throw httpClientErrorException;
        }
    }

    /**
     * {@inheritDoc}
     */
    public InputStream makeMultipartPost(String path, MultiMap<String, File> files, MultiMap<String, String> params) throws IOException,
            HttpException {
        byte[] boundary = generateMultipartBoundary();
        MultiMap<String, String> header = createMultiPartHeader(boundary);
        ReversibleOutputStream rev = writeMultipartBody(files, params, boundary);
        return makeRequestWithBody(path, HttpMethod.POST, header, EMPTY_MAP, rev.reverse()).head();
    }

    /**
     * Escreve o corpo de um post multipart
     * 
     * @param files os arquivos a serem enviados
     * @param params os parametros a serem enviados
     * @param boundary os bytes que servem de separador
     * @return {@link ReversibleOutputStream}
     * @throws IOException e
     */
    private ReversibleOutputStream writeMultipartBody(MultiMap<String, File> files, MultiMap<String, String> params, byte[] boundary)
            throws IOException {
        ReversibleOutputStream rev = new ReversibleOutputStream(BUFFER_LIMIT);
        writeFiles(files, boundary, rev);
        writeMultipartParams(params, boundary, rev);
        rev.write(CRLF);
        rev.write(SEPARATOR);
        rev.write(boundary);
        rev.write(SEPARATOR);
        rev.write(CRLF);
        rev.flush();
        rev.close();
        return rev;
    }

    /**
     * Cria o cabecalho de um request multipart
     * 
     * @param boundary o limite a ser usado
     * @return {@link MultiMap}
     * @throws UnsupportedEncodingException e
     */
    private MultiMap<String, String> createMultiPartHeader(byte[] boundary) throws UnsupportedEncodingException {
        MultiMap<String, String> header = new ListMultiMap<String, String>();
        header.add(CONTENT_TYPE, "multipart/form-data; boundary=" + new String(boundary, HEADER_ENCODING));
        header.add(CONTENT_TYPE, "boundary=" + new String(boundary, HEADER_ENCODING));
        header.add(CONTENT_TYPE, "charset=" + HEADER_ENCODING);
        return header;
    }

    /**
     * Escreve os parametros de post como parte do request de multipart
     * 
     * @param params {@link MultiMap}
     * @param boundary o limite
     * @param output a saida
     * @throws IOException e
     */
    private void writeMultipartParams(MultiMap<String, String> params, byte[] boundary, OutputStream output) throws IOException {
        Collection<Entry<String, Collection<String>>> entries = params.entries();
        for (Entry<String, Collection<String>> entry : entries) {
            byte[] fieldName = entry.getKey().getBytes(HEADER_ENCODING);
            for (String value : entry.getValue()) {
                output.write(CRLF);
                output.write(SEPARATOR);
                output.write(boundary);
                output.write(CRLF);
                output.write(CONTENT_DISPOSITION);
                output.write(QUOTE);
                output.write(fieldName);
                output.write(QUOTE);
                output.write(CRLF);
                output.write("Content-Type: text/plain; charset=UTF-8".getBytes(HEADER_ENCODING));
                output.write(CRLF);
                output.write(CRLF);
                output.write(value.getBytes("UTF-8"));
            }
        }
    }

    /**
     * Escreve os arquivos no request
     * 
     * @param files os arquivos a serem enviados
     * @param boundary o limite
     * @param rev output
     * @throws IOException e
     */
    private void writeFiles(MultiMap<String, File> files, byte[] boundary, OutputStream rev) throws IOException {
        Collection<Entry<String, Collection<File>>> entries = files.entries();
        for (Entry<String, Collection<File>> entry : entries) {
            byte[] fieldName = entry.getKey().getBytes(HEADER_ENCODING);
            for (File file : entry.getValue()) {
                rev.write(CRLF);
                rev.write(SEPARATOR);
                rev.write(boundary);
                rev.write(CRLF);
                rev.write(CONTENT_DISPOSITION);
                rev.write(QUOTE);
                rev.write(fieldName);
                rev.write(QUOTE);
                rev.write(FILENAME);
                rev.write(QUOTE);
                rev.write(file.getName().getBytes(HEADER_ENCODING));
                rev.write(QUOTE);
                rev.write(CRLF);
                rev.write(CRLF);
                FileInputStream fin = new FileInputStream(file);
                try {
                    IOUtils.copyFully(fin, rev);
                } finally {
                    fin.close();
                }

            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public Reader makeMultipartPostStringResponse(String path, MultiMap<String, File> files, MultiMap<String, String> params)
            throws IOException, HttpException {
        byte[] boundary = generateMultipartBoundary();
        MultiMap<String, String> header = createMultiPartHeader(boundary);
        ReversibleOutputStream rev = writeMultipartBody(files, params, boundary);
        return makeRequestStringResponse(path, HttpMethod.POST, header, EMPTY_MAP, rev.reverse());
    }

    /**
     * {@inheritDoc}
     */
    public String encodeParameters(MultiMap<String, String> queryParameters) throws IOException {
        return encodeQueryParameters(queryParameters);
    }

    /**
     * Generates a random multipart boundary string.
     * 
     * @return byte[]
     */
    public static byte[] generateMultipartBoundary() {
        Random rand = new Random();
        final int maxLen = 11;
        final int start = 30;
        byte[] bytes = new byte[rand.nextInt(maxLen) + start]; // a random size from 30 to 40
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)];
        }
        return bytes;
    }

    /**
     * {@inheritDoc}
     */
    public Tuple<InputStream, MultiMap<String, String>> makeRequest(String path, HttpMethod method,
            MultiMap<String, String> headerParameters, MultiMap<String, String> queryParameters) throws HttpException, IOException {
        return makeRequestWithBody(path, method, headerParameters, queryParameters, null);
    }

    /**
     * {@inheritDoc}
     */
    public Reader makeRequestStringResponse(String path, HttpMethod method, MultiMap<String, String> headerParameters,
            MultiMap<String, String> queryParameters) throws HttpException, IOException {
        if (HttpMethod.POST == method) {
            return makeRequestStringResponse(path, method, headerParameters, EMPTY_MAP, new StringInputStream(
                    encodeParameters(queryParameters), "UTF-8"));
        } else {
            return makeRequestStringResponse(path, method, headerParameters, queryParameters, null);
        }
    }

    /**
     * {@inheritDoc}
     */
    public Reader makeRequestStringResponse(String path, HttpMethod method, MultiMap<String, String> headerParameters,
            MultiMap<String, String> queryParameters, InputStream requestBody) throws HttpException, IOException {
        Tuple<InputStream, MultiMap<String, String>> response =
                makeRequestWithBody(path, method, headerParameters, queryParameters, requestBody);
        String charset = getCharset(response.tail());
        return new InputStreamReader(response.head(), charset);
    }

    /**
     * Faz um request, devolvendo o corpo da mensagem e os cabecalhos
     * 
     * @param path String
     * @param method {@link HttpMethod}
     * @param headerParameters os cabecalhos a serem mandados
     * @param queryParameters os parametros
     * @param requestBody o corpo do request. Pode ser <code>null</code>
     * @return {@link Tuple} de {@link InputStream} para {@link MultiMap}
     * @throws HttpException e
     * @throws IOException e
     */
    public Tuple<InputStream, MultiMap<String, String>> makeRequestWithBody(String path, HttpMethod method,
            MultiMap<String, String> headerParameters, MultiMap<String, String> queryParameters, InputStream requestBody)
            throws HttpException, IOException {
        return makeRequestWithBody(path, method, headerParameters, queryParameters, requestBody, MAX_RETRIES_COUNT, null);
    }

    /**
     * Faz um request, devolvendo o corpo da mensagem e os cabecalhos
     * 
     * @param path String
     * @param method {@link HttpMethod}
     * @param headerParameters os cabecalhos a serem mandados
     * @param queryParameters os parametros
     * @param requestBody o corpo do request. Pode ser <code>null</code>
     * @return {@link Tuple} de {@link InputStream} para {@link MultiMap}
     * @param retryCount int
     * @param thrown the thrown exception
     * @throws HttpException e
     * @throws IOException e
     */
    protected Tuple<InputStream, MultiMap<String, String>> makeRequestWithBody(String path, HttpMethod method,
            MultiMap<String, String> headerParameters, MultiMap<String, String> queryParameters, InputStream requestBody, int retryCount,
            HttpException thrown) throws HttpException, IOException {
        if (retryCount <= 0) {
            if (thrown != null) {
                throw thrown;
            }
            final int httpResponse = 400;
            throw new HttpException(httpResponse, "maximum retry count reached");
        }
        try {
            return this.doMakeRequestWithBody(path, method, headerParameters, queryParameters, requestBody);
        } catch (RetryRequestException e) {
            return makeRequestWithBody(path, method, headerParameters, queryParameters, requestBody, retryCount - 1, null);
        }
    }

    /**
     * Faz um request, devolvendo o corpo da mensagem e os cabecalhos
     * 
     * @param path String
     * @param method {@link HttpMethod}
     * @param headerParameters os cabecalhos a serem mandados
     * @param queryParameters os parametros
     * @param requestBody o corpo do request. Pode ser <code>null</code>
     * @return {@link Tuple} de {@link InputStream} para {@link MultiMap}
     * @throws HttpException e
     * @throws IOException e
     */
    protected abstract Tuple<InputStream, MultiMap<String, String>> doMakeRequestWithBody(String path, HttpMethod method,
            MultiMap<String, String> headerParameters, MultiMap<String, String> queryParameters, InputStream requestBody)
            throws HttpException, IOException;

    /**
     * Procura o charset
     * 
     * @param tail {@link MultiMap}
     * @return String
     */
    public static String getCharset(MultiMap<String, String> tail) {
        Collection<? extends String> collection = tail.get(CONTENT_TYPE);
        if (collection == null || collection.isEmpty()) {
            return EncodingUtils.ISO_CHARSET;
        }
        for (String string : collection) {
            if (string.contains("charset=")) {
                return findCharset(string);
            }
        }
        return EncodingUtils.ISO_CHARSET;
    }

    /**
     * Procura uma tag charset= ou devolve ISO-8859-10
     * 
     * @param typeField String
     * @return String
     */
    public static String findCharset(String typeField) {
        String[] pieces = typeField.split(";");
        for (String string : pieces) {
            String possibleCharset = string.trim();
            int indexOf = possibleCharset.indexOf("charset=");
            if (indexOf >= 0) {
                return possibleCharset.substring(indexOf + "charset=".length()).trim();
            }
        }
        return HEADER_ENCODING;
    }

}
